package com.gitee.simpleplus.jproxy;

import static org.apache.commons.lang3.CharUtils.LF;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtNewMethod;

public class MutiFactoryMethodGenerator {
	private static final Logger log = LoggerFactory.getLogger(MutiFactoryMethodGenerator.class);
	private static final char LINE_BREAK;
	static {
		LINE_BREAK = log.isInfoEnabled() ? LF : ' ';
	}

	public static <T> Class<T> genFactory(Class<T> inf, Set<Class<?>> classList) {
		if (!inf.isInterface()) {
			throw new RuntimeException("inf:" + inf.getName() + " is not a interface");
		}
		MutiFactory fa = inf.getAnnotation(MutiFactory.class);
		if (fa == null) {
			throw new RuntimeException("inf:" + inf.getName() + " donnt exist MutiFactory Annotation");
		}

		try {
			ClassPool cp = ClassPool.getDefault();
			CtClass ctClazzInf = cp.get(inf.getName());

			CtClass ctClazzChild = cp.makeClass(inf.getName() + "$$Proxy");

			ctClazzChild.addInterface(ctClazzInf);

			Class<? extends Annotation> value = fa.value();

			Method[] declaredMethods = inf.getDeclaredMethods();

			for (Method method : declaredMethods) {
				genFactory(cp, ctClazzChild, method, classList, inf.getName());
			}

			return ctClazzChild.toClass();
		} catch (Exception e1) {
			throw new RuntimeException(e1);
		}
	}

	private static <T> void genFactory(ClassPool cp, CtClass ctClazzChild, Method m, Set<Class<?>> classList,
			String infClazzName) throws Exception {
		MutiFactory annotation = m.getAnnotation(MutiFactory.class);
		if (annotation == null) {
			if (m.isDefault())
				return;
		}

		int typesLen = m.getParameterCount();

		Class<?>[] types = m.getParameterTypes();
		Class<?>[] typesConstructor = new Class<?>[types.length - 1];
		System.arraycopy(types, 1, typesConstructor, 0, typesConstructor.length);

		StringBuilder body = new StringBuilder("");

		if (annotation != null) {

			Class<? extends Annotation> anClazz = annotation.value();
			if (!anClazz.equals(Annotation.class)) {
				body.append("switch($1){").append(LINE_BREAK);

				classList.stream().filter(cl -> cl.isAnnotationPresent(anClazz)).forEach(c -> {
					try {
						c.getConstructor(typesConstructor);// check if exist construct

						Object invoke = anClazz.getDeclaredMethods()[0].invoke(c.getAnnotation(anClazz));
						if (invoke.getClass().isArray()) {
							for (int i = 0; i < Array.getLength(invoke); i++) {
								appendCaseStatement(typesLen, body, c, Array.get(invoke, i));
							}
						} else {
							appendCaseStatement(typesLen, body, c, invoke);
						}
					} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
							| NoSuchMethodException | SecurityException e) {
						throw new RuntimeException(e);
					}

				});
				body.append("default: return null;").append(LINE_BREAK).append("}");
				log.info("inf:{}, methodName:{}, body:{}", infClazzName, m.getName(), body);
			}
		}
		addCtMethod(ctClazzChild, m, cp, body);
	}

	private static void appendCaseStatement(int typesLen, StringBuilder body, Class<?> c, Object val) {
		body.append("case ").append(val).append(": return new ").append(c.getName()).append("(");

		for (int j = 0; j < typesLen - 1; j++) {
			body.append(j == 0 ? "" : ",").append("$").append(j + 2);
		}

		body.append(");").append(LINE_BREAK);
	}

	private static CtClass[] covertClazz(ClassPool cp, Class<?>[] clazzes) throws Exception {
		CtClass[] ctClazz = new CtClass[clazzes.length];
		for (int i = 0; i < clazzes.length; i++) {
			ctClazz[i] = cp.get(clazzes[i].getName());
		}
		return ctClazz;
	}

	private static void addCtMethod(CtClass ctClazzChild, Method m, ClassPool cp, StringBuilder body) throws Exception {
		ctClazzChild.addMethod(CtNewMethod.make(cp.get(m.getReturnType().getName()), m.getName(),
				covertClazz(cp, m.getParameterTypes()), covertClazz(cp, m.getExceptionTypes()), body.toString(),
				ctClazzChild));

	}
}
